package jm.easyconfig;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.ref.WeakReference;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.annotation.PostConstruct;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.event.EventListener;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.AbstractFileResolvingResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PropertiesLoaderSupport;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public class CfgContextEnvironmentAware
		implements BeanPostProcessor, ApplicationContextAware, EnvironmentAware, DisposableBean {

	static ConfigurableEnvironment environment;
	static ApplicationContext applicationContext;
	static MySqlConfigSource configSource;
	static ConfigUpdater publisher;
	private static CfgContextEnvironmentAware instance;

	Map<String, List<BeanPropPair>> code2BeanProp = new HashMap<>();
	Map<Object, BeanPostRefreshInfo> bean2PostRefreshInfo = new WeakHashMap<Object, BeanPostRefreshInfo>();

	public CfgContextEnvironmentAware() {
		if (instance != null) {
			CfgHelper.logger.error(
					"Only one instance of CfgContextEnvironmentAware is allowed.{} is created duplicatedly after {}.",
					this, instance);
		} else {
			instance = this;
			CfgHelper.logger.info("{} singleton is created for dynamic configuration paramaters refresh.", this);
		}
	}

	public static CfgContextEnvironmentAware getSharedInstance() {
		return instance;
	}

	ProfilesHelper profilesHelper;

	static MySqlConfigSource getConfigSource() {
		return configSource;
	}

	static ConfigUpdater getPublisher() {
		return publisher;
	}

	public Environment getEnvironment() {
		return environment;
	}

	SecurityMode securityMode;

	@Override
	public void setEnvironment(Environment env) {
		if (instance != this) {
			CfgHelper.logger.error("setEnvironment is called on {}, which is created duplicatedly after {}.", this,
					instance);
		}

		try {
			environment = (ConfigurableEnvironment) env;

			loadEasyconfigProperties();

			securityMode = SecurityMode.valueOf(environment.getProperty("easyconfig.security.mode", "self"));

			Map<String, Object> decryptedProps = decryptedBackendCipher(environment);
			if (decryptedProps.size() > 0)
				environment.getPropertySources()
						.addFirst(new MapPropertySource(SysProps.DECRYPTED_PROPERTY_SOURCE_NAME, decryptedProps));

			profilesHelper = new ProfilesHelper(environment).loadLocalCache();

			// refresh from the decrypted config and local cache
			// todo refactor to remove SysProps
			SysProps.inst.refresh(env);

		} catch (Throwable e) {
			CfgHelper.logger.error("Error when to invoke setEnvironment, message:{} ", e.getMessage());
		}
	}

	static void loadEasyconfigProperties() {
		// step 0 : get root easyconfig.properties
		loadProertiesIntoEnvironment("/easyconfig.properties");
		loadProertiesIntoEnvironment("/config/easyconfig.properties");

		// step 1 get the value of easyconfig.boot-profile
		String profileHolder = environment.getProperty("easyconfig.boot-profile-holder", "easyconfig.boot-profile");
		String profile = environment.getProperty(profileHolder);

		if (profile == null) {
			CfgHelper.logger.info("[easyconfig.boot-profile-holder] is {}, but [{}] is not cofigured.", profileHolder,
					profileHolder);
			return;
		}

		profile = profile.trim();

		// step 2 load easyconfig.properties of the current profile
		loadProertiesIntoEnvironment("/config/easyconfig-" + profile + ".properties");
		loadProertiesIntoEnvironment("/" + profile + "/easyconfig.properties");
		loadProertiesIntoEnvironment("/easyconfig-" + profile + ".properties");
	}

	static void loadProertiesIntoEnvironment(String nameInPath) {
		try {
			AbstractFileResolvingResource resource = new ClassPathResource(nameInPath);
			loadProertiesIntoEnvironment(resource, "classpath:" + nameInPath);

			resource = new UrlResource("file:." + nameInPath);
			loadProertiesIntoEnvironment(resource, "file:." + nameInPath);

		} catch (Throwable e) {
			CfgHelper.logger.error("Failed to load :{},message:{}", nameInPath, e.getMessage());
		}
	}

	static void loadProertiesIntoEnvironment(AbstractFileResolvingResource resource, String SourceName)
			throws IOException {
		if (!resource.exists()) {
			return;
		}

		Properties properties = new Properties();

		properties.load(resource.getInputStream());

		MutablePropertySources propertySources = environment.getPropertySources();
		PropertiesPropertySource propertySource = new PropertiesPropertySource(SourceName, properties);
		if (propertySources.contains("systemEnvironment"))
			propertySources.addAfter("systemEnvironment", propertySource);
		else
			propertySources.addFirst(propertySource);

		CfgHelper.logger.info("{} is loaded.", SourceName);
	}

	public ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	@Override
	public void setApplicationContext(ApplicationContext ctx) throws BeansException {
		if (instance != this) {
			CfgHelper.logger.error("setApplicationContext is called on {}, which is created duplicatedly after {}.",
					this, instance);
		}

		try {
			CfgContextEnvironmentAware.applicationContext = ctx;

			configSource = new MySqlConfigSource(environment.getProperty("easyconfig.client", "main"));
			if (configSource.startup()) {
				publisher = ConfigUpdater.startup(configSource, this);
			}

			// dynamic refresh from remote data source
			this.register(SysProps.inst);

			ApplicationContext parent = ctx.getParent();
			if (parent != null) {
				CfgHelper.logger.info("{} is created in a child container {}, whose parent is {} ", this, ctx, parent);

				String[] names = parent.getBeanDefinitionNames();
				for (String n : names) {
					this.register(parent.getBean(n), n + "@parent");
				}

				CfgHelper.logger.info("{} registered [{}] beans in parent container of {}", this, names.length, parent);
			}

		} catch (Throwable e) {
			CfgHelper.logger.error("Error when to invoke setApplicationContext, message:{} ", e.getMessage());
		}
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		try {
			register(bean, beanName);
		} catch (Throwable e) {
			e.printStackTrace();
			CfgHelper.logger.error("Error when to invoke postProcessBeforeInitialization({},{}), message:{} ", bean,
					beanName, e.getMessage());
		}
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	static Map<String, Object> decryptedBackendCipher(ConfigurableEnvironment env) {
		Map<String, Object> map = new ConcurrentHashMap<String, Object>();

		String[] keys = env.getProperty(SysProps.CIPHER_KEYS_PROP_KEY, String[].class, SysProps.stringArrayEmpty);

		if (keys.length == 0)
			return map;

		for (String key : keys) {
			key = key.trim();
			if (!StringUtils.hasLength(key))
				continue;
			String content = env.getProperty(key, "");
			map.put(key, CfgHelper.decrypt(content));
		}

		return map;
	}

	// It's rarely called while crashing down in spring boot application
	@Override // if @PreDestroy is added ,will be called twice?
	public void destroy() throws Exception {
		if (instance != this)
			return;

		if (publisher != null)
			publisher.close();
	}

	public boolean register(Object bean) {
		return register(bean, bean != null ? bean.toString() : "");
	}

	// register bean and assign its properties with environment variables
	public boolean register(Object bean, String beanName) {
		if (bean == null)// check when client invoked
			return false;

		Class<?> clazz = null;

		boolean modified = false;

		// @Value @EasyValue @ValueExt for field
		clazz = bean.getClass();
		while (Object.class != clazz && clazz != null) {
			for (Field field : clazz.getDeclaredFields()) {
				modified = registerAccessObject(bean, beanName, field) || modified;
			}
			clazz = clazz.getSuperclass();
		}

		// @Value @EasyValue @ValueExt for for setField
		// NOTICE: don't support the annotation from interface
		clazz = bean.getClass();
		while (Object.class != clazz && clazz != null) {
			for (Method method : clazz.getDeclaredMethods()) {
				modified = registerAccessObject(bean, beanName, method) || modified;
			}
			clazz = clazz.getSuperclass();
		}

		registerForPostRefresh(bean);

		if (modified)
			invokePostRefresh(bean, true);

		return modified;
	}

	private boolean registerAccessObject(Object bean, String beanName, AccessibleObject accessObj) {
		String propHolder = null;
		Annotation annotation = null;
		if (SysProps.inst.valueEnabled && accessObj.isAnnotationPresent(Value.class)) {
			annotation = accessObj.getAnnotation(Value.class);
			propHolder = ((Value) annotation).value();
		} else if (accessObj.isAnnotationPresent(EasyValue.class)) {
			annotation = accessObj.getAnnotation(EasyValue.class);
			propHolder = ((EasyValue) annotation).value();

			if (!propHolder.startsWith("${") || !propHolder.endsWith("}")) {
				throw new RuntimeException("the value attribute of @EasyValue must be ${...} format.");
			}
		} else if (accessObj.isAnnotationPresent(ValueExt.class)) {
			annotation = accessObj.getAnnotation(ValueExt.class);
			propHolder = ((ValueExt) annotation).value();
			if (!propHolder.startsWith("${") || !propHolder.endsWith("}")) {
				throw new RuntimeException("the value attribute of @ValueExt must be ${...} format.");
			}
		} else
			return false;

		boolean modified = false;
		List<String> keyList = pickupPropertyCode(propHolder, 0);
		for (String key : keyList) {
			BeanPropPair pair = new BeanPropPair(bean, accessObj, propHolder);
			pair.beanName = beanName;
			pair.annotation = annotation;
			// todo updateValue should be updated not more than once ?
			modified = pair.updateValue(environment) || modified;

			List<BeanPropPair> list = code2BeanProp.computeIfAbsent(key, (k) -> new LinkedList<>());
			boolean existed = false;
			for (BeanPropPair p : list) {
				if (p.bean() == bean && p.accessObj == accessObj) {
					existed = true;
					CfgHelper.logger.error("Donot registered {}({}) repeatedly.", bean, beanName);
					break;
				}
			}
			if (!existed)
				list.add(pair);
		}

		return modified;
	}

	// NOTICE: Not support the annotation in super class
	private void registerForPostRefresh(Object bean) {
		boolean needed = false;
		BeanPostRefreshInfo info = new BeanPostRefreshInfo();

		if (bean instanceof InitializingBean) {
			info.isInitializingBean = true;
			needed = true;
		}

		for (Method method : bean.getClass().getDeclaredMethods()) {

			if (method.isAnnotationPresent(PostRefresh.class)) {
				info.postRefresh = method;
				needed = true;
			}

			if (method.isAnnotationPresent(PostConstruct.class)) {
				info.postConstruct = method;
				needed = true;
			}
		}

		if (needed) {
			this.bean2PostRefreshInfo.put(bean, info);
		}
	}

	private void invokePostRefresh(Object bean, boolean initialPhase) {
		BeanPostRefreshInfo info = this.bean2PostRefreshInfo.get(bean);
		if (info == null)
			return;

		try {
			if (initialPhase) {
				if (info.postRefresh != null && !info.isInitializingBean && info.postConstruct == null) {
					info.postRefresh.invoke(bean);
					return;
				}
				// postConstruct and InitializingBean will be invoked by Spring during initial
				// phase
			} else {
				if (info.postRefresh != null) {
					info.postRefresh.invoke(bean);
					return;
				}

				if (info.isInitializingBean) {
					((InitializingBean) bean).afterPropertiesSet();
				}

				if (info.postConstruct != null) {
					info.postConstruct.invoke(bean);
				}
			}

		} catch (Exception e) {
			CfgHelper.logger.error("Error to invoke PostRefresh {} of bean {}. message:{}", info, bean, e.getMessage());
		}
	}

	static List<String> pickupPropertyCode(String value, int begin) {
		int start = value.indexOf("${", begin) + 2;
		if (start < 2) {
			return new ArrayList<>();
		}

		int middle = value.indexOf(":", start);
		int end = value.indexOf("}", start);

		String key;
		if (middle > 0 && middle < end) {
			// include default value
			key = value.substring(start, middle);
		} else {
			// do not include default value
			key = value.substring(start, end);
		}

		List<String> keys = pickupPropertyCode(value, end);
		keys.add(key);
		return keys;
	}

	static String clientUniqueName(ConfigurableEnvironment env) {
		if (env == null) // check for some test scene
			return "";

		String name = env.getProperty("easyconfig.application-name", "");

		if (!StringUtils.hasLength(name))
			name = env.getProperty("easyconfig.datasource.url", "");

		if (!StringUtils.hasLength(name))
			name = env.getProperty("spring.datasource.url", "");

		name += ":";
		name += env.getProperty("easyconfig.client", "main");

//		StringBuilder sb = new StringBuilder();
//		name.chars().forEach(ch -> sb.append(Character.isJavaIdentifierPart(ch) ? (char) ch : '-'));
//		name = sb.toString();

		return name;
	}

	private static String _nodeName;

	static String nodeName() {
		if (_nodeName != null)
			return _nodeName;

		try {
			InetAddress ia;
			String ip = "";
			ia = InetAddress.getLocalHost();
			ip = ia.getHostAddress();
			String port = environment.getProperty("server.port", "unknown");

			// nodeName will be used as profile name, process id per restarting

//			if ("unknown".equals(port)) {
//				RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
//				// ip@pid@hostname
//				_nodeName = String.format("%s@%s", ip, runtimeMXBean.getName());
//			} else 
			{
				String name = "";
				name = ia.getHostName();
				// ip:port@hostname
				_nodeName = String.format("%s:%s@%s", ip, port, name);
			}

		} catch (Throwable e) {
			CfgHelper.logger.info("error when to get host address. {}", e.getMessage());
			e.printStackTrace();
		}

		return _nodeName;
	}

	@EventListener
	void configRefreshListener(ConfigRefreshEvent event) {
		try {
			CfgHelper.logger.info("received a ConfigRefreshEvent with {} config item.", event.properties.size());

			List<ConfigItemCore> updated = this.profilesHelper.updateEnvProperties(event.properties);

			updateBeanProperties(updated);

			if (SysProps.inst.localcacheEnabled && updated.size() > 0)
				profilesHelper.storeLocalCache();

		} catch (Throwable ex) {
			CfgHelper.logger.error("{}", ex);
		}
	}

	// todo refactor to remove
	@Deprecated // for unit test only
	void updateSystem(ConfigItemCore... properties) {
		profilesHelper.updateEnvProperties(Arrays.asList(properties));
		updateBeanProperties(Arrays.asList(properties));
	}

	@Deprecated // for unit test
	public void updateSystem(Map<String, Object> keyValueMap) {
		List<ConfigItemCore> properties = new LinkedList<ConfigItemCore>();
		keyValueMap.forEach((k, v) -> {
			properties.add(new ConfigItemCore().withCode(k).withValue((String) v));
		});

		updateSystem(properties.toArray(new ConfigItemCore[properties.size()]));
	}

	void updateBeanProperties(List<ConfigItemCore> properties) {
		Set<Object> updatedSet = new HashSet<Object>();

		for (ConfigItemCore item : properties) {
			BeanPropPair.foreach(code2BeanProp.get(item.item_code), pair -> {
				if (pair.updateValue(environment))
					updatedSet.add(pair.bean());
			});
		}

		for (Object bean : updatedSet) {
			this.invokePostRefresh(bean, false);
		}
	}

	List<Map<String, Object>> listToLoad(String profile) {
		final String prof = StringUtils.hasLength(profile) ? profile : "default";

		Map<String, ConfigItem> source = new HashMap<String, ConfigItem>();
		configSource.configList().forEach(map -> {
			if (Objects.equals(map.item_profile, prof))
				source.put(map.item_code, map);
		});

		List<Map<String, Object>> current = new LinkedList<Map<String, Object>>();

		for (Entry<String, List<BeanPropPair>> entry : this.code2BeanProp.entrySet()) {
			if (source.containsKey(entry.getKey()) || entry.getValue().size() == 0)
				continue;

			Map<String, Object> map = new HashMap<String, Object>();
			map.put("item_code", entry.getKey());

			String item_name = null;
			// todo improve item_value /item_schema to complex placeholder
			BeanPropPair firstPair = entry.getValue().get(0);
			if (firstPair.accessObj.isAnnotationPresent(EasyValue.class)) {
				item_name = firstPair.accessObj.getAnnotation(EasyValue.class).name();
			} else if (firstPair.accessObj.isAnnotationPresent(ValueExt.class)) {
				item_name = firstPair.accessObj.getAnnotation(ValueExt.class).name();
			}

			if (!StringUtils.hasLength(item_name))
				item_name = entry.getKey();

			Object item_value = environment.getProperty(entry.getKey());
			if (item_value == null) {
				item_value = firstPair.value();
			}

			map.put("item_name", item_name);
			map.put("item_value",
					item_value != null && item_value.getClass() != String.class ? CfgHelper.jsonWrite(item_value)
							: item_value);
			map.put("item_schema", type2JsonSchema(firstPair.propType(), null, null));
			map.put("item_status", 0);

			StringBuilder sb = new StringBuilder();
			BeanPropPair.foreach(entry.getValue(), pair -> {
				sb.append(pair.propText());
				sb.append(",");
			});

			sb.deleteCharAt(sb.length() - 1);
			map.put("usedBy", sb.toString());

			current.add(map);

		} // end of for

		current.sort((a, b) -> {
			String ak = a.get("item_code").toString();
			String bk = b.get("item_code").toString();
			return ak.compareTo(bk);
		});

		return current;
	}

	static Map<String, Object> type2JsonSchema(Class<?> type, Class<?> eType, LinkedList<Class<?>> loop) {
		Map<String, Object> map = new HashMap<String, Object>();

		if (loop == null) {
			loop = new LinkedList<Class<?>>();
		}

		if (type == null) {
//			map.put("type", "object"); type can be any thing or unknown
			return map;
		} else if (type == null || loop.contains(type)) {
			map.put("type", "object");
			return map;
		}

		loop.push(type);

		if (type == String.class) {
			map.put("type", "string");
		} else if (type == Integer.class || type == int.class || type == Long.class || type == long.class
				|| type == Short.class || type == short.class) {
			map.put("type", "integer");
		} else if (type == Boolean.class || type == boolean.class) {
			map.put("type", "boolean");
			map.put("enum", new boolean[] { true, false });
		} else if (type == Float.class || type == float.class || type == Double.class || type == double.class
				|| type == BigDecimal.class) {
			map.put("type", "number");
		} else if (Map.class.isAssignableFrom(type)) {
			map.put("type", "object");
		} else if (type.isArray()) {
			map.put("type", "array");
			map.put("items", type2JsonSchema(type.getComponentType(), null, loop));
		} else if (type.isEnum()) {
			Object[] enums = type.getEnumConstants();
			map.put("type", "string");
			map.put("enum", enums);
		} else if (Collection.class.isAssignableFrom(type)) {
			map.put("type", "array");
			map.put("items", type2JsonSchema(eType, null, loop));
		} else if (conversionService().canConvert(String.class, type)) {
			map.put("type", "string");
		} else if (Object.class.isAssignableFrom(type) && Object.class != type) {
			map.put("type", "object");
			HashMap<String, Object> properties = new HashMap<String, Object>();
			map.put("properties", properties);

			while (Object.class != type && type != null) {
				Field[] fields = type.getDeclaredFields();
				for (Field f : fields) {

					if (Modifier.isStatic(f.getModifiers()))
						continue;

					if (Collection.class.isAssignableFrom(f.getType())) {
						ParameterizedType ptype = (ParameterizedType) f.getGenericType();
						HashMap<String, Object> sub = new HashMap<String, Object>();
						properties.put(f.getName(), sub);
						sub.put("type", "array");
						sub.put("items", type2JsonSchema((Class<?>) ptype.getActualTypeArguments()[0], null, loop));
					} else
						properties.put(f.getName(), type2JsonSchema(f.getType(), null, loop));
				}

				type = type.getSuperclass();
			}
		}

		loop.pop();

		return map;
	}

	static DefaultConversionService conversionService;

	static DefaultConversionService conversionService() {
		if (conversionService == null)
			conversionService = new DefaultConversionService();

		return conversionService;
	}

	public PropDetail inspect(String prop) {

		PropDetail detail = new PropDetail();

		Iterator<PropertySource<?>> itrt = environment.getPropertySources().iterator();

		while (itrt.hasNext()) {
			PropertySource<?> propertySource = itrt.next();

			if (!propertySource.containsProperty(prop))
				continue;

			if (propertySource.getClass().getName().equals(
					"org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource")) {
				continue; // duplicated property source.
			}

			PropInSource p = new PropInSource().withPropValue(propertySource.getProperty(prop))
					.withSourceName(propertySource.getName()).withSourceClass(propertySource.getClass());

			detail.definedList.addLast(p);
		}

		if (placeholderConfigurerList == null)
			placeholderConfigurerList = PlaceholderConfigurerHolder.propertiesList(applicationContext);

		for (PlaceholderConfigurerHolder configurer : placeholderConfigurerList) {
			for (PropertiesHolder pro : configurer.propertiesArray) {
				if (!pro.properties.containsKey(prop))
					continue;

				PropInSource propInSource = new PropInSource()
						.withSourceClass(configurer.beanName + "@" + configurer.configurer.getClass());
				try {
					propInSource.withSourceName(pro.resource.getFilename() + "@" + pro.resource.getURI().toString());
				} catch (IOException e) {
					e.printStackTrace();
				}
				propInSource.withPropValue(pro.properties.getProperty(prop));
				detail.propertiesList.addLast(propInSource);
			}
		}

		if (this.code2BeanProp.containsKey(prop)) {
			List<BeanPropPair> list = this.code2BeanProp.get(prop);

			BeanPropPair.foreach(list, pair -> {
				detail.usedList.addLast(new PropInBean().withBeanPropPair(pair));
			});

		}

		return detail;
	}

	PlaceholderConfigurerHolder[] placeholderConfigurerList = null;
}

class PropertiesHolder {
	public Resource resource;
	public Properties properties;

	PropertiesHolder withResource(Resource resource) {
		this.resource = resource;
		return this;
	}

	PropertiesHolder withProperties(Properties properties) {
		this.properties = properties;
		return this;
	}
}

class PlaceholderConfigurerHolder {
	public PropertiesLoaderSupport configurer;
	public String beanName;
	public PropertiesHolder[] propertiesArray;

	PlaceholderConfigurerHolder withConfigurer(PropertiesLoaderSupport configurer) {
		this.configurer = configurer;
		return this;
	}

	PlaceholderConfigurerHolder withPropertiesArray(PropertiesHolder[] propertiesArray) {
		this.propertiesArray = propertiesArray;
		return this;
	}

	PlaceholderConfigurerHolder withBeanName(String beanName) {
		this.beanName = beanName;
		return this;
	}

	static Field getField(Class<?> clazz, String name) {
		Field finded = null;
		while (finded == null && Object.class != clazz && clazz != null) {
			for (Field field : clazz.getDeclaredFields()) {
				if (field.getName().equals(name)) {
					finded = field;
					break;
				}
			}
			clazz = clazz.getSuperclass();
		}
		return finded;
	}

	static Object getFieldValue(Object bean, String name) {
		Field field = getField(bean.getClass(), name);
		boolean accessible = field.isAccessible();
		field.setAccessible(true);
		Object val = null;
		try {
			val = field.get(bean);
		} catch (Throwable e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
		field.setAccessible(accessible);

		return val;
	}

	static PlaceholderConfigurerHolder[] propertiesList(ApplicationContext ctx) {
		List<PlaceholderConfigurerHolder> configurerArray = new LinkedList<PlaceholderConfigurerHolder>();

		propertiesList(ctx.getParent(), configurerArray);

		propertiesList(ctx, configurerArray);

		return configurerArray.toArray(new PlaceholderConfigurerHolder[configurerArray.size()]);
	}

	static void propertiesList(ApplicationContext ctx, List<PlaceholderConfigurerHolder> configurerArray) {
		if (ctx == null)
			return;

		String[] names = ctx.getBeanNamesForType(PropertiesLoaderSupport.class, false, false);

		for (int n = 0; n < names.length; n++) {
			String name = names[n];

			PropertiesLoaderSupport pls = (PropertiesLoaderSupport) ctx.getBean(name);
			Resource[] locations = (Resource[]) getFieldValue(pls, "locations");

			if (locations == null)// spring will create one automatically without locations sometimes
				continue;

			LinkedList<PropertiesHolder> propResourceList = new LinkedList<PropertiesHolder>();

			for (Resource r : locations) {
				if (!r.exists())
					continue;

				try {
					Properties p = new Properties();

					p.load(r.getInputStream());

					propResourceList.addLast(new PropertiesHolder().withResource(r).withProperties(p));

				} catch (IOException e) {
					CfgHelper.logger.error("Failed to load resource of {},message is  {}", r.getFilename(),
							e.getMessage());
				}
			}

			configurerArray.add(new PlaceholderConfigurerHolder().withConfigurer(pls)
					.withPropertiesArray(propResourceList.toArray(new PropertiesHolder[propResourceList.size()]))
					.withBeanName(name));
		}
	}
}

class BeanPostRefreshInfo {
	Method postRefresh;
	boolean isInitializingBean;
	Method postConstruct;
	// how to get init-method from spring? AbstractBeanDefinition

	@Override
	public String toString() {
		return "BeanPostRefreshInfo [postRefresh=" + postRefresh + ", isInitializingBean=" + isInitializingBean
				+ ", postConstruct=" + postConstruct + "]";
	}
}

class ConfigRefreshEvent extends ApplicationEvent {
	private static final long serialVersionUID = 1L;
	List<ConfigItemCore> properties;
	boolean includeAll;

	public ConfigRefreshEvent(Object source, List<ConfigItemCore> properties, boolean includeAll) {
		super(source);
		this.properties = properties;
		this.includeAll = includeAll;
	}
}

class BeanPropPair {
	WeakReference<Object> beanRef;
	AccessibleObject accessObj;/* field or setter */
	String propHolder;// placeholder for @Value @EasyValue @ValueExt

	String beanName;
	Annotation annotation;

	BeanPropPair(Object bean, AccessibleObject accessObj, String propholder) {
		super();
		this.beanRef = new WeakReference<>(bean);
		// todo method must be a setter
		this.accessObj = accessObj;
		this.propHolder = propholder;
	}

	@Override
	public String toString() {
		return "BeanPropPair [bean =" + bean() + ", accessObj=" + accessObj + ", propHolder=" + propHolder + "]";
	}

	Object bean() {
		return beanRef.get();
	}

	Object value() {
		return valueOfAccessObj(this.accessObj, bean());
	}

	boolean updateValue(Environment env) {
		try {
			String itemValue = env.resolvePlaceholders(propHolder);

			if (itemValue.indexOf("${") >= 0) {
				CfgHelper.logger.warn("{} is not resolved in bean of {}", itemValue, this.beanRef.get().toString());
				return false;
			}

			Object oldv = this.value();

			Object newv;
			Class<?> type;

			if (this.accessObj instanceof Field) {
				type = ((Field) this.accessObj).getType();
			} else {
				type = ((Method) this.accessObj).getParameterTypes()[0];
			}

			if (type == String.class) {
				newv = itemValue;
			} else if (type == Boolean.class || type == boolean.class) {
				newv = Boolean.parseBoolean(itemValue);
			} else if (type == Integer.class || type == int.class) {
				newv = Integer.parseInt(itemValue);
			} else if (itemValue.trim().startsWith("[") || itemValue.trim().startsWith("{")) {
				newv = CfgHelper.jsonRead(itemValue, type);
			} else {
//				newv = DefaultConversionService.getSharedInstance().convert(itemValue, type);
				newv = CfgContextEnvironmentAware.conversionService().convert(itemValue, type);
			}

			if (Objects.deepEquals(oldv, newv))
				return false;

			return (boolean) accessIt(this.accessObj, () -> {
				try {
					if (this.accessObj instanceof Field) {
						((Field) this.accessObj).set(bean(), newv);
					} else {
						((Method) this.accessObj).invoke(bean(), newv);
					}
				} catch (Throwable e) {
					e.printStackTrace();
					throw new RuntimeException(e);
				}

				return true;
			});

		} catch (Throwable e) {
			CfgHelper.logger.error("Error when to update value of {}. message:{}", this, e.getMessage());
		}
		return false;
	}

	Class<?> propType() {
		return propType(this.accessObj);
	}

	static Class<?> propType(AccessibleObject accessObj) {
		return accessObj instanceof Field ? ((Field) accessObj).getType()
				: ((Method) accessObj).getParameters()[0].getType();
	}

	static String propTypeName(AccessibleObject accessObj) {
		Class<?> type = propType(accessObj);
		if (type.isArray()) {
			return type.getComponentType().getSimpleName() + "[]";
		}
		return type.getName();
	}

	String propText() {
		if (this.accessObj instanceof Field) {
			Field field = (Field) this.accessObj;
			return String.format("%s %s@%s", field.getType().getSimpleName(), field.getName(),
					bean().getClass().getSimpleName());
		}
		// else is setter method
		Method setter = (Method) this.accessObj;
		Class<?> type = setter.getParameters()[0].getType();
		return String.format("%s(%s %s)@%s", setter.getName(), type.getSimpleName(),
				setter.getParameters()[0].getName(), bean().getClass().getSimpleName());
	}

	static void foreach(List<BeanPropPair> list, Consumer<BeanPropPair> consumer) {
		if (list == null)// for traversal convenience
			return;

		Iterator<BeanPropPair> it = list.iterator();
		while (it.hasNext()) {
			BeanPropPair pair = it.next();
			Object beanStrong = pair.bean(); // prevent to gc temporarily
			if (beanStrong == null) {
				it.remove();
				CfgHelper.logger.info("!!!bean is garbage collected.{}!!!", pair);
			} else {
				consumer.accept(pair);
			}
		}
	}

	static Object valueOfAccessObj(AccessibleObject accessObj, Object bean) {
		if (accessObj instanceof Field) {
			return fieldValue((Field) accessObj, bean);
		}

		if (accessObj instanceof Method) {
			String methodName = ((Method) accessObj).getName();
			Method getter = null;

			Class<?> clazz = bean.getClass();

			if (methodName.startsWith("set")) {
				getter = ReflectionUtils.findMethod(clazz, "get" + methodName.substring(3));
				if (getter != null)
					return getterValue(getter, bean);

				String fieldName = methodName.substring(3);
				if (fieldName.length() == 0)
					return null;
				char ch = fieldName.charAt(0);
				if (!Character.isJavaIdentifierStart(ch) || !Character.isUpperCase(ch))
					return null;
				fieldName = Character.toLowerCase(ch) + fieldName.substring(1);
				Field field = ReflectionUtils.findField(clazz, fieldName);
				return field != null ? fieldValue(field, bean) : null;
			}
		}

		return null;
	}

	static Object getterValue(Method getter, Object bean) {
		return accessIt(getter, () -> {
			try {
				return getter.invoke(bean);
			} catch (Throwable e) {
				e.printStackTrace();
				CfgHelper.logger.error("Error to invoke getter, message:{}", e.getMessage());
			}
			return null;
		});
	}

	static Object fieldValue(Field field, Object bean) {
		return accessIt(field, () -> {
			try {
				return field.get(bean);
			} catch (Throwable e) {
				e.printStackTrace();
				CfgHelper.logger.error("Error to get field value, message:{}", e.getMessage());
			}
			return null;
		});
	}

	static Object accessIt(AccessibleObject ao, Supplier<Object> function) {
		Object ret = null;

		boolean accessible = ao.isAccessible();
		if (!accessible)
			ao.setAccessible(true);

		ret = function.get();

		if (!accessible)
			ao.setAccessible(false);

		return ret;
	}
}// end of BeanPropPair

class PropDetail {
	public String appNode = CfgContextEnvironmentAware.nodeName();
	public LinkedList<PropInSource> definedList = new LinkedList<PropInSource>();
	public LinkedList<PropInSource> propertiesList = new LinkedList<PropInSource>();
	public LinkedList<PropInBean> usedList = new LinkedList<PropInBean>();
}

class PropInSource {
	public Object propValue;
	public String sourceName;
	public String sourceClass;

	PropInSource withPropValue(Object propValue) {
		this.propValue = propValue;
		return this;
	}

	PropInSource withSourceName(String sourceName) {
		this.sourceName = sourceName;
		return this;
	}

	PropInSource withSourceClass(Class<?> clazz) {
		this.sourceClass = clazz.getName();
		return this;
	}

	PropInSource withSourceClass(String name) {
		this.sourceClass = name;
		return this;
	}
}

class PropInBean {
	public String beanName;
	public String beanString;
	public String beanClass;
	public String jarPath;

	public String propDecl;
	public String propValue;// affected by default or complex expression, eg {test.port:8080}
	public String annotation;

	PropInBean withBeanPropPair(BeanPropPair pair) {
		this.beanName = pair.beanName;
		this.beanString = pair.bean().toString();
		this.beanClass = pair.bean().getClass().getName();
		this.jarPath = pair.bean().getClass().getProtectionDomain().getCodeSource().getLocation().getFile();

		this.propDecl = declaration(pair);
		Object val = pair.value();
		this.propValue = val instanceof String ? val.toString() : CfgHelper.jsonWrite(val);

		if (pair.annotation instanceof Value) {
			Value v = (Value) pair.annotation;
			this.annotation = String.format("@Value(value=\"%s\")", v.value());
		} else if (pair.annotation instanceof EasyValue) {
			EasyValue v = (EasyValue) pair.annotation;
			this.annotation = String.format("@EasyValue(value=\"%s\",name=\"%s\")", v.value(), v.name());
		} else if (pair.annotation instanceof ValueExt) {
			ValueExt v = (ValueExt) pair.annotation;
			this.annotation = String.format("@ValueExt(value=\"%s\",name=\"%s\")", v.value(), v.name());
		}

		return this;
	}

	static Class<?> propType(AccessibleObject accessObj) {
		return accessObj instanceof Field ? ((Field) accessObj).getType()
				: ((Method) accessObj).getParameters()[0].getType();
	}

	static String propTypeName(AccessibleObject accessObj) {
		Class<?> type;
		ParameterizedType pt;
		if (accessObj instanceof Field) {
			Field f = (Field) accessObj;
			type = f.getType();
			// todo deal with generic type.
			if (!Objects.equals(f.getType(), f.getGenericType())) {
				pt = (ParameterizedType) f.getGenericType();
			}
		} else {
			Method m = (Method) accessObj;
			type = m.getParameters()[0].getType();
		}

		// todo deal with generic
		if (type.isArray()) {
			return type.getComponentType().getSimpleName() + "[]";
		}
		return type.getSimpleName();
	}

	static String declaration(BeanPropPair pair) {
		if (pair.accessObj instanceof Field) {
			Field field = (Field) pair.accessObj;

			return String.format("%s %s %s;", Modifier.toString(field.getModifiers()), propTypeName(pair.accessObj),
					field.getName());
		}
		// else is setter method
		Method setter = (Method) pair.accessObj;
		Class<?> type = setter.getParameters()[0].getType();

		return String.format("%s %s %s(%s %s){...}", Modifier.toString(setter.getModifiers()),
				setter.getReturnType().getSimpleName(), setter.getName(), type.getSimpleName(),
				setter.getParameters()[0].getName());
	}

}
